Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(async-flow): add more context to vow rejection #10359

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

michaelfig
Copy link
Member

@michaelfig michaelfig commented Oct 29, 2024

closes: #10147

Description

When an asyncFlow guest calls the host through its replay membrane, keep an ephemeral weakMapStore from any result hostVow to the target, method, (and eventually, whether it is an "eventual" result).

If that hostVow is rejected with an error, annotate the error as described in #10147.

Security Considerations

Error annotations are only revealed to the base causal console, which cannot be captured in vat code (unlike the consoleRecorder shim that is set up during tests in this PR).

Scaling Considerations

No significant increase yet.

Documentation Considerations

n/a

Testing Considerations

n/a

Upgrade Considerations

n/a

@michaelfig michaelfig force-pushed the mfig-10147-flow-rejects branch from ffdb74c to cc5a1fb Compare October 29, 2024 01:53
Copy link

cloudflare-workers-and-pages bot commented Oct 29, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: 733042d
Status: ✅  Deploy successful!
Preview URL: https://61d5d4cf.agoric-sdk.pages.dev
Branch Preview URL: https://mfig-10147-flow-rejects.agoric-sdk.pages.dev

View logs

@michaelfig michaelfig force-pushed the mfig-10147-flow-rejects branch 2 times, most recently from 0740c55 to fc1f17a Compare November 1, 2024 02:02
@michaelfig michaelfig force-pushed the mfig-10147-flow-rejects branch 2 times, most recently from a66a9ce to 49b4c9f Compare November 13, 2024 16:24
@michaelfig michaelfig force-pushed the mfig-10147-flow-rejects branch from 49b4c9f to 588d227 Compare November 13, 2024 17:00
@michaelfig michaelfig marked this pull request as ready for review November 13, 2024 17:55
@michaelfig michaelfig requested a review from a team as a code owner November 13, 2024 17:55
@michaelfig michaelfig self-assigned this Nov 13, 2024
@michaelfig michaelfig added the asyncFlow related to membrane-based replay and upgrade of async functions label Nov 13, 2024
Comment on lines 16 to 17
export const makeTemplateStringsArray = strings =>
harden(assign([...strings], { raw: strings }));
Copy link
Member

@erights erights Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever, but the attributes matter. The raw should be non-enumerable. How about

Suggested change
export const makeTemplateStringsArray = strings =>
harden(assign([...strings], { raw: strings }));
export const makeTemplateStringsArray = strings => {
const result = [...strings];
defineProperty(result, 'raw', {
value: strings, // Note: same rather than raw version
enumerable: false, // not needed, but good to be explicit
});
return harden(result);
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finessed your suggestion a little bit to avoid needing to coerce result with a typecast.

@@ -22,6 +22,12 @@ export type FlowState =
export type Guest<T extends unknown = any> = T;
export type Host<T extends Passable = Passable> = T;

export type HostCall = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just from the name, I expected a HostCall to also include the arguments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll explain why this is not the case in a comment (basically, the replay membrane doesn't need them to produce good-enough errors).

@erights erights self-requested a review November 13, 2024 19:34
@@ -11,10 +11,21 @@ const BEST_GUESS_ID_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
* Return an object that mimics a template strings array.
*
* @param {string[]} strings
* @param {string[]} [raw] optional raw strings, defaults to `strings`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

// Make the raw property non-enumerable.
defineProperty(result, 'raw', {
value: raw,
enumerable: false, // not needed, but good to be explicit
Copy link
Member

@erights erights Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the "finessed" change, the explicit enumerable: false, is needed.

Suggested change
enumerable: false, // not needed, but good to be explicit
enumerable: false,

@erights erights self-requested a review November 13, 2024 22:12
@mhofman
Copy link
Member

mhofman commented Nov 19, 2024

keep a durable weakMapStore from any result hostVow to the target, method, (and eventually, whether it is an "eventual" result).

I have not reviewed yet, but I'm surprised we need a durable store for this. Since we always replay, why can't we use an ephemeral store?

@michaelfig
Copy link
Member Author

keep a durable weakMapStore [...]

I have not reviewed yet, but I'm surprised we need a durable store for this. Since we always replay, why can't we use an ephemeral store?

Ah, good catch. I'll try with an ephemeral store and ensure it still passes tests.

Copy link
Member

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to parse the test for this, but here are my preliminary comments on the implementation.

if (typeof v === 'function') {
return {
[p](...args) {
if (recording) recording.push({ thisArg: target, method: p, args });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't thisArg a little misleading as a name if it records the proxy's target.

Comment on lines +18 to +21
apply(target, thisArg, args) {
if (recording) recording.push({ thisArg, func: target, args });
return Reflect.apply(target, thisArg, args);
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about this? Do we ever have a callable console?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the tag config additions are in commit before makeReplayMembrane adds support / documentation for that config.

args,
});

t.true(recording.every(entry => entry.thisArg === originalConsole));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by construction of the recording console, this can only be true, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this file seem to be testing behavior introduced in a later commit.

Comment on lines +345 to +349
initHostCall(vow, {
target: hostTarget,
method: optVerb,
eventual: true,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah with eventual send it's gonna be more difficult to record on the guest side since we don't see the guest anymore.

);
if (hostVowToCall) {
const hostVowCap = toPassableCap(hostVow);
if (hostVowToCall.has(hostVowCap)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case would the map not have the cap?

if (hostVowToCall) {
const hostVowCap = toPassableCap(hostVow);
if (hostVowToCall.has(hostVowCap)) {
const hostCall = hostVowToCall.get(hostVowCap);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to leave the host call in the map after rejection? I think doFulfill should remove from the map as well.

@@ -118,6 +118,7 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
bijection, // membrane's guest-host mapping
outcomeKit: makeVowKit(), // outcome of activation as host vow
isDone: false, // persistently done
hostVowToCall: zone.detached().weakMapStore('hostVowToCall'),
Copy link
Member

@mhofman mhofman Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides changing to an ephemeral store, I think we shouldn't burden ourselves with weak collections and their gc costs since we have delimited lifetimes for these host call entries, and don't need to retain them past settlement. Since the replay membrane is keeping the result vow/promises live anyway, there is no reason to assume those might be dropped earlier either.

* @param {any[]} args
* @returns {[TemplateStringsArray, ...any[]]}
*/
export const inlineTemplateArgs = (tmpl, ...args) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if the ergonomics wouldn't be simpler with a wrapper for the original template tag function:

inlineArgs(X)`foo bar ${inlinedArg}`;

@mhofman mhofman self-requested a review November 19, 2024 16:09
Copy link
Member

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a little hard to digest the test. Wondering how we can simplify its reading.

Comment on lines +52 to +55
const consStack = i => {
const re = indexToStackRegex[i];
delete indexToStackRegex[i];
t.truthy(re, `recorded entry ${i} has a stack regex`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not accept the re as an argument instead of this definition indirection?

Suggested change
const consStack = i => {
const re = indexToStackRegex[i];
delete indexToStackRegex[i];
t.truthy(re, `recorded entry ${i} has a stack regex`);
const consStack = (i, re) => {
t.truthy(re, `recorded entry ${i} has a stack regex`);

const error = (...args) => cons('error', ...args);
const group = (...args) => cons('group', ...args);

const expectedRecording = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little weird to see assertions being made during the declaration of the expected value, but then I'm not sure how to do it any better. I also think that it'd be better to run the main "deepEqual" assertion before any nested assertions for specific entries.

I'm wondering if something like the helpers returning a tuple of "method + args" and an optional thunk to run extra assertions. The "method +args" would be used to build an expected result that can be used first with deepEqual. The thunks if present would then be invoked one by one on each recorded entry. The thunk could have a signature of (recordedArgs, i) so that the helper definitions don't need to close over the recording input.

Comment on lines +215 to +219
test.serial('test host vow rejection logging', async t => {
annihilate();
const zone1 = makeDurableZone(getBaggage(), 'durableRoot');
await testRejectionPlay(t, zone1);
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd like to see this test also written with a restart where the vow is only resolved in the new incarnation.

async guestMethod(gOrch7) {
const p2 = gOrch7.vow();

await eventLoopIteration();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this new crank?

Comment on lines +186 to +190
setImmediate(() => {
gOrch7.reject(Error('should be seen'));
});

await eventLoopIteration();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while semantically equivalent to an await eventLoopIteration() followed by a call to reject, I assume this is written like this to put the rejection on an empty stack even if the engine has async stack traces? Can we add a comment to this effect?


await eventLoopIteration();

const caughtP = p2.catch(e => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to sanity check that the promise doesn't fulfill.

Suggested change
const caughtP = p2.catch(e => {
const caughtP = p2.then(
v => t.fail(`vow fulfilled with value ${v}`),
e => {

3: error('Error#1 ERROR_NOTE:', 'from host error', '(Error#2)'),
4: error(
'Error#1 ERROR_NOTE:',
'from flow',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it read better as?

Suggested change
'from flow',
'during execution of flow',

[recLen17 - 6]: error('Error#4:', 'should be seen'),
[recLen17 - 5]: consStack(recLen17 - 5),
[recLen17 - 4]: cons('groupEnd'),
[recLen17 - 3]: error('Error#3:', 'this stack'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have a message a little more descriptive? maybe:

Suggested change
[recLen17 - 3]: error('Error#3:', 'this stack'),
[recLen17 - 3]: error('Error#3:', 'AsyncFlow1 definition site'),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
asyncFlow related to membrane-based replay and upgrade of async functions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve async-flow debugging when host rejects vow
3 participants